﻿// Copyright (c) 2020-2021 百小僧, Baiqian Co.,Ltd.
// TimeCrontab is licensed under Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
//             https://gitee.com/dotnetchina/TimeCrontab/blob/master/LICENSE
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.

using System;
using System.Collections.Generic;

namespace TimeCrontab
{
    /// <summary>
    /// Cron 字段值含 / 字符解析器
    /// </summary>
    /// <remarks>
    /// <para>表示从某值开始，每隔固定值触发，该字符支持在 Cron 所有字段域中设置</para>
    /// </remarks>
    internal sealed class StepParser : ICronParser, ITimeParser
    {
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="start">起始值</param>
        /// <param name="steps">步长</param>
        /// <param name="kind">Cron 字段种类</param>
        /// <exception cref="TimeCrontabException"></exception>
        public StepParser(int start, int steps, CrontabFieldKind kind)
        {
            // 验证步长有效性：不能小于或等于0，且不能大于 Cron 字段种类取值最大值
            var minimum = Constants.MinimumDateTimeValues[kind];
            var maximum = Constants.MaximumDateTimeValues[kind];
            if (steps <= 0 || steps > maximum)
            {
                throw new TimeCrontabException(string.Format("Steps = {0} is out of bounds for <{1}> field.", steps, Enum.GetName(typeof(CrontabFieldKind), kind)));
            }

            Start = start;
            Steps = steps;
            Kind = kind;

            // 控制循环起始值，并不一定从 Start 开始
            var loopStart = Math.Max(start, minimum);

            // 计算所有满足间隔步长计算的解析器
            var parsers = new List<SpecificParser>();
            for (var evalValue = loopStart; evalValue <= maximum; evalValue++)
            {
                if (IsMatch(evalValue))
                {
                    parsers.Add(new SpecificParser(evalValue, Kind));
                }
            }

            SpecificParsers = parsers;
        }

        /// <summary>
        /// Cron 字段种类
        /// </summary>
        public CrontabFieldKind Kind { get; }

        /// <summary>
        /// 起始值
        /// </summary>
        public int Start { get; }

        /// <summary>
        /// 步长
        /// </summary>
        public int Steps { get; }

        /// <summary>
        /// 所有满足间隔步长计算的解析器
        /// </summary>
        public IEnumerable<SpecificParser> SpecificParsers { get; }

        /// <summary>
        /// 判断当前时间是否符合 Cron 字段种类解析规则
        /// </summary>
        /// <param name="datetime">当前时间</param>
        /// <returns><see cref="bool"/></returns>
        public bool IsMatch(DateTime datetime)
        {
            // 获取不同 Cron 字段种类对应时间值
            var evalValue = Kind switch
            {
                CrontabFieldKind.Second => datetime.Second,
                CrontabFieldKind.Minute => datetime.Minute,
                CrontabFieldKind.Hour => datetime.Hour,
                CrontabFieldKind.Day => datetime.Day,
                CrontabFieldKind.Month => datetime.Month,
                CrontabFieldKind.DayOfWeek => datetime.DayOfWeek.ToCronDayOfWeek(),
                CrontabFieldKind.Year => datetime.Year,
                _ => throw new ArgumentOutOfRangeException(nameof(datetime), Kind, null),
            };

            return IsMatch(evalValue);
        }

        /// <summary>
        /// 获取 Cron 字段种类当前值的下一个发生值
        /// </summary>
        /// <param name="currentValue">时间值</param>
        /// <returns><see cref="int"/></returns>
        /// <exception cref="TimeCrontabException"></exception>
        public int? Next(int currentValue)
        {
            // 由于天、月、周计算复杂，所以这里排除对它们的处理
            if (Kind == CrontabFieldKind.Day
                || Kind == CrontabFieldKind.Month
                || Kind == CrontabFieldKind.DayOfWeek)
            {
                throw new TimeCrontabException("Cannot call Next for Day, Month or DayOfWeek types.");
            }

            // 默认递增步长为 1
            int? newValue = currentValue + 1;

            // 获取下一个匹配的发生值
            var maximum = Constants.MaximumDateTimeValues[Kind];
            while (newValue < maximum && !IsMatch(newValue.Value))
            {
                newValue++;
            }

            return newValue > maximum ? null : newValue;
        }

        /// <summary>
        /// 存储起始值，避免重复计算
        /// </summary>
        private int? FirstCache { get; set; }

        /// <summary>
        /// 获取 Cron 字段种类字段起始值
        /// </summary>
        /// <returns><see cref="int"/></returns>
        /// <exception cref="TimeCrontabException"></exception>
        public int First()
        {
            // 判断是否缓存过起始值，如果有则跳过
            if (FirstCache.HasValue)
            {
                return FirstCache.Value;
            }

            // 由于天、月、周计算复杂，所以这里排除对它们的处理
            if (Kind == CrontabFieldKind.Day
                || Kind == CrontabFieldKind.Month
                || Kind == CrontabFieldKind.DayOfWeek)
            {
                throw new TimeCrontabException("Cannot call First for Day, Month or DayOfWeek types.");
            }

            var maximum = Constants.MaximumDateTimeValues[Kind];
            var newValue = 0;

            // 获取首个符合的起始值
            while (newValue < maximum && !IsMatch(newValue))
            {
                newValue++;
            }

            // 验证起始值有效性
            if (newValue > maximum)
            {
                throw new TimeCrontabException(
                    string.Format("Next value for {0} on field {1} could not be found!",
                    ToString(),
                    Enum.GetName(typeof(CrontabFieldKind), Kind))
                );
            }

            // 缓存起始值
            FirstCache = newValue;
            return newValue;
        }

        /// <summary>
        /// 将解析器转换成字符串输出
        /// </summary>
        /// <returns><see cref="string"/></returns>
        public override string ToString()
        {
            return string.Format("{0}/{1}", Start == 0 ? "*" : Start.ToString(), Steps);
        }

        /// <summary>
        /// 判断是否符合间隔或带步长间隔解析规则
        /// </summary>
        /// <param name="evalValue">当前值</param>
        /// <returns><see cref="bool"/></returns>
        private bool IsMatch(int evalValue)
        {
            return (evalValue - Start) % Steps == 0;
        }
    }
}